/*
 * Su-yun Information Technology, Co.,Ltd CONFIDENTIAL
 *
 * (C) Copyright Su-yun Information Technology, Co.,Ltd. 2021 All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains the property of
 * Su-yun Information Technology, Co.,Ltd. The intellectual property herein
 * are protected by trade secret, patent law or copyright law. Dissemination
 * of this information or reproduction of this material is strictly forbidden
 * unless prior written permission is obtained from Su-yun Information Technology.
 *
 * https://suyunplat.com/
 *
 */
package com.xtiim.battery.discharge.util;

import cn.hutool.core.util.StrUtil;
import com.xtiim.battery.discharge.common.exception.Asserts;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Date time tool utils
 *
 */
public class TimeUtil {

    public static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";

    /**
     * Convert string to date
     *
     * @param dateTimeString  date time string
     * @param format  date time format
     * @return  date object if parameter is qualified, null otherwise
     */
    public static Date strToDate(String dateTimeString, String format) {
        if (StrUtil.isBlank(dateTimeString)) {
            return null;
        }
        DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(format);
        DateTime dateTime = null;
        try {
            dateTime = dateTimeFormatter.parseDateTime(dateTimeString);
        } catch (Exception e) {
            return null;
        }
        return dateTime.toDate();
    }

    /**
     * Convert string to date with standard format "yyyy-MM-dd HH:mm:ss"
     *
     * @param dateTimeString date time string
     * @return date object if parameter is qualified, null otherwise
     */
    public static Date strToDate(String dateTimeString) {
        return strToDate(dateTimeString, STANDARD_FORMAT);
    }

    /**
     * Convert date to string
     *
     * @param date  date object
     * @param format  date time format
     * @return  date time string
     */
    public static String dateToStr(Date date, String format) {
        DateTime dateTime = new DateTime(date);
        return dateTime.toString(format);
    }

    /**
     * Convert date to string with standard date time format "yyyy-MM-dd HH:mm:ss"
     *
     * @param date  date object
     * @return date time string
     */
    public static String dateToStr(Date date) {
        return dateToStr(date, STANDARD_FORMAT);
    }

    /**
     * Parse byte array into {@link Date}
     * |y|M|d|h|m|s|
     *
     * @param bytes
     * @return
     */
    public static Date bytesToDate(byte[] bytes) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, bytes[0] + 2000);
        calendar.set(Calendar.MONTH, bytes[1] - 1);
        calendar.set(Calendar.DAY_OF_MONTH, bytes[2]);
        calendar.set(Calendar.HOUR_OF_DAY, bytes[3]);
        calendar.set(Calendar.MINUTE, bytes[4]);
        calendar.set(Calendar.SECOND, bytes[5]);
        calendar.set(Calendar.MILLISECOND, 0);

        return calendar.getTime();
    }

    /**
     * Handle time string in format yyyyMMddHHmmss.
     * Output bytes:
     * |yy|yy|M|d|H|m|s|
     *
     * @param timeStr
     * @return
     */
    public static byte[] time2byte(String timeStr) {
        byte[] bytes = new byte[7];
        Integer y1 = Integer.valueOf(timeStr.substring(0, 2));
        Integer y2 = Integer.valueOf(timeStr.substring(2, 4));
        Integer M = Integer.valueOf(timeStr.substring(4, 6));
        Integer d = Integer.valueOf(timeStr.substring(6, 8));
        Integer H = Integer.valueOf(timeStr.substring(8, 10));
        Integer m = Integer.valueOf(timeStr.substring(10, 12));
        Integer s = Integer.valueOf(timeStr.substring(12, 14));

        bytes[0] = y1.byteValue();
        bytes[1] = y2.byteValue();
        bytes[2] = M.byteValue();
        bytes[3] = d.byteValue();
        bytes[4] = H.byteValue();
        bytes[5] = m.byteValue();
        bytes[6] = s.byteValue();

        return bytes;
    }

    /**
     * Get the month list of last year, like
     * 2021-09
     * 2021-08
     * 2021-07
     *
     * @return month list
     */
    public static String[] getMonthListInLastYear() {
        String[] months = new String[12];

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int i = 11;
        do {
            Date time = calendar.getTime();
            String month = TimeUtil.dateToStr(time, "yyyy-MM");
            calendar.add(Calendar.MONTH, -1);
            months[i] = month;
            i--;
        } while (i >= 0);
        return months;
    }

    /**
     * Get the days list in last 7 days, including today, like
     * 2021-09-21
     * 2021-09-20
     * ...
     * 2021-09-15
     *
     * @return day list
     */
    public static String[] getDayListInLast7Days() {
        String[] days = new String[7];

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int i = 0;
        do {
            Date time = calendar.getTime();
            String day = TimeUtil.dateToStr(time, "yyyy-MM-dd");
            calendar.add(Calendar.DATE, -1);
            days[i] = day;
            i++;
        } while (i < 7);
        return days;
    }

    /**
     * Validate time range and then convert string into {@link Date}
     * array.
     *
     * @param beginTime begin time
     * @param endTime end time
     * @return time range in {@link Date} array
     */
    public static Date[] validateAndConvertTimeRange(String beginTime, String endTime) {
        if (StrUtil.isBlank(beginTime)) {
            Asserts.failInvalidParam("Invalid begin time.");
        }

        if (StrUtil.isBlank(endTime)) {
            Asserts.failInvalidParam("Invalid end time.");
        }

        Date begin = TimeUtil.strToDate(beginTime);
        if (begin == null) {
            Asserts.failInvalidParam("Invalid begin time.");
        }

        Date end = TimeUtil.strToDate(endTime);
        if (end == null) {
            Asserts.failInvalidParam("Invalid end time.");
        }

        if (begin.after(end)) {
            Asserts.failInvalidParam("Begin time should be earlier than end time.");
        }

        return new Date[] {begin, end};
    }

    /**
     * get the current date and generate dates num days before or after {@link Date}
     * @param offset the offset corresponds to days
     * @return time in {@link Date}
     */
    public static Date getBeforeOrAfterDate(Date time, Integer offset) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(time);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DATE);
        calendar.set(year, month, day, 0, 0, 0);
        calendar.add(Calendar.DATE, offset);
        return calendar.getTime();
    }

    public static Date getBeforeOrAfterMidDate(Date time, Integer offset) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(time);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DATE);
        calendar.set(year, month, day, 12, 0, 0);
        calendar.add(Calendar.DATE, offset);
        return calendar.getTime();
    }

    /**
     * get the current date and generate dates num months before or after {@link Date}
     * @param offset the offset corresponds to days
     * @return time in {@link Date}
     */
    public static Date getBeforeOrAfterMonth(Date time, Integer offset) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(time);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        calendar.set(year, month, 1, 0, 0, 0);
        calendar.add(Calendar.MONTH, offset);
        return calendar.getTime();
    }

    /**
     * Transform the integer (minutes level) to datetime format for example 720->12:00:00
     * @return
     */
    public static Date getIntTime(Integer intTime) {
        if (intTime.intValue() >= 1440 || intTime.intValue() < 0) {
            Asserts.failInvalidParam("Invalid Integer Time.");
        }
        Integer hour = Integer.valueOf(intTime.intValue() / 60);
        Integer minute = Integer.valueOf(intTime.intValue() % 60);
        Calendar calendar = Calendar.getInstance();
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DATE);
        calendar.set(year, month, day, hour, minute, 0);
        return calendar.getTime();
    }

    /**
     * compare the hour and minute
     * @return true date1 <= date2 by hour and minute
     */
    public static boolean lessThanHHMM(Date date1, Date date2) {
        Calendar calendar1 = Calendar.getInstance();
        calendar1.setTime(date1);
        Calendar calendar2 = Calendar.getInstance();
        calendar2.setTime(date2);
        int h1 = calendar1.get(Calendar.HOUR_OF_DAY);
        int h2 = calendar2.get(Calendar.HOUR_OF_DAY);
        int m1 = calendar1.get(Calendar.MINUTE);
        int m2 = calendar2.get(Calendar.MINUTE);
        if (h1 < h2) {
            return true;
        } else if (h1 > h2) {
            return false;
        } else if (m1 <= m2) {
            return true;
        } else {
            return  false;
        }
    }

    /**
     * get the calendar of one date without hour, minute, second, millisecond
     * @param date
     * @return
     */
    private static Calendar getYYMMDD(Date date) {
        Calendar c = Calendar.getInstance();
        if (date == null) {
            c.setTime(new Date());
        } else {
            c.setTime(date);
        }
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        return c;
    }

    /**
     * get the day difference between two dates
     * @param date1
     * @param date2
     * @return
     */
    private static Long getDayDiff(Date date1, Date date2) {
        Long diffInMill = date2.getTime() - date1.getTime();
        return TimeUnit.DAYS.convert(diffInMill, TimeUnit.MILLISECONDS);
    }

    /**
     * Get the number of days of one month
     * @param year
     * @param month index of month start from 0
     * @return
     */
    private static int getNumDaysOfMonth(int year, int month) {
        Calendar c = Calendar.getInstance();
        c.set(year, month + 1, 1);
        c.add(Calendar.DAY_OF_YEAR, -1);
        return c.get(Calendar.DAY_OF_MONTH);
    }

    /**
     * check if day is periodic to date begin
     * @param begin
     * @param day
     * @param periodicValue
     * @param periodicUnit
     * @return
     */
    private static boolean isPeriodic(Calendar begin, Calendar day, Integer periodicValue, String periodicUnit) {
        int year1 = begin.get(Calendar.YEAR);
        int year2 = day.get(Calendar.YEAR);

        int month1 = begin.get(Calendar.MONTH);
        int month2 = day.get(Calendar.MONTH);

        int day1 = begin.get(Calendar.DAY_OF_MONTH);
        int day2 = day.get(Calendar.DAY_OF_MONTH);

        if (periodicUnit.equals("day")) {
            int dayDiff = getDayDiff(begin.getTime(), day.getTime()).intValue();
            if (dayDiff % periodicValue == 0) { return true; }
        } else if (periodicUnit.equals("week")) {
            int dayDiff = getDayDiff(begin.getTime(), day.getTime()).intValue();
            if (dayDiff % (periodicValue * 7) == 0) { return true; }
        } else if (periodicUnit.equals("month")) {
            int monthDiff = 12 * (year2 - year1) + month2 - month1;
            if (monthDiff % periodicValue != 0) { return false; }
            if (day1 == day2) { return true; }
            int numDaysOfMonth2 = getNumDaysOfMonth(year2, month2);
            if (numDaysOfMonth2 < day1 && day2 == numDaysOfMonth2) { return true; }
        } else if (periodicUnit.equals("year")) {
            int yearDiff = year2 - year1;
            if (yearDiff % periodicValue != 0) { return false; }
            if (month1 != month2) { return false; }
            if (day1 == day2) { return true; }
            int numDaysOfMonth2 = getNumDaysOfMonth(year2, month2);
            if (numDaysOfMonth2 < day1 && day2 == numDaysOfMonth2) { return true; }
        }

        return false;
    }

    /**
     * return the dates in recent days defined by preDay.
     * the returned dates are periodic of the beginTime.
     * this is typically used for periodic maintain order generation
     * and periodic spot check order generation
     * @param beginTime
     * @param endTime
     * @param periodicValue
     * @param periodicUnit periodic unit, must be one of year/month/week/day
     * @param preDay
     * @return
     */
    public static List<Date> getRecentPeriodicDates(Date beginTime, Date endTime,
                                                    Integer periodicValue, String periodicUnit, Integer preDay) {
        if (beginTime == null) {
            Asserts.fail("Periodic begin time cannot be null.");
        }

        List<Date> recentPeriodicDates = new ArrayList<>();

        Calendar beginCalendar = null;
        Calendar endCalendar = null;
        if (beginTime != null) { beginCalendar = getYYMMDD(beginTime); }
        if (endTime != null) { endCalendar = getYYMMDD(endTime); }

        for (int i = 0; i <= preDay; i++) {
            Calendar daysLater = getYYMMDD(null);
            daysLater.add(Calendar.DAY_OF_YEAR, i);

            // check if it's in range [beginTime, endTime]
            if (daysLater.before(beginCalendar)) { continue; }
            if (endCalendar != null && daysLater.after(endCalendar)) { continue; }

            // check if it's periodic
            if (isPeriodic(beginCalendar, daysLater, periodicValue, periodicUnit)) {
                daysLater.set(Calendar.HOUR_OF_DAY, 23);
                daysLater.set(Calendar.MINUTE, 59);
                daysLater.set(Calendar.SECOND, 59);
                recentPeriodicDates.add(daysLater.getTime());
            }
        }
        return recentPeriodicDates;
    }

    /**
     * Get the month list of one year, like
     * 2021-01
     * 2021-02
     * 2021-03
     * ...
     * 2022-01
     * @return month list
     */
    public static String[] getMonthPointInOneYear(String year) {
        if (StrUtil.isBlank(year)) {
            Asserts.failInvalidParam("Invalid year param.");
        }

        String[] months = new String[13];
        Date yearTime = TimeUtil.strToDate(year, "yyyy");
        if (yearTime == null) {
            Asserts.failInvalidParam("Invalid year param.");
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(yearTime);
        int i = 0;
        do {
            Date time = calendar.getTime();
            String month = TimeUtil.dateToStr(time, "yyyy-MM");
            calendar.add(Calendar.MONTH, 1);
            months[i] = month;
            i++;
        } while (i < 13);
        return months;
    }

    /**
     * Get the day list of one month, like
     * 2021-01-01
     * 2021-01-02
     * ...
     * 2021-01-31
     * @return month list
     */
    public static String[] getDayPointInOneMonth(String month) {
        if (StrUtil.isBlank(month)) {
            Asserts.failInvalidParam("Invalid month param");
        }

        Date monthTime = TimeUtil.strToDate(month, "yyyy-MM");
        if (monthTime == null) {
            Asserts.failInvalidParam("Invalid month param");
        }
        Calendar daysCounter = Calendar.getInstance();
        daysCounter.setTime(monthTime);
        daysCounter.roll(Calendar.DATE, -1);
        int daysNum = daysCounter.get(Calendar.DATE);
        String[] days = new String[daysNum + 1];
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(monthTime);
        int i = 0;
        do {
            Date time = calendar.getTime();
            String day = TimeUtil.dateToStr(time, "yyyy-MM-dd");
            calendar.add(Calendar.DATE, 1);
            days[i] = day;
            i++;
        } while (i < daysNum + 1);
        return days;
    }

    /**
     * Get hour list of one day, like
     * 2021-01-01 00:00:00
     * 2021-01-01 01:00:00
     * ...
     * 2021-01-01 23:00:00
     * @return month list
     */

    public static String[] getHourPointInOneDay(String day) {
        if (StrUtil.isBlank(day)) {
            Asserts.failInvalidParam("Invalid day param");
        }
        String[] hours = new String[25];
        Date dayTime = TimeUtil.strToDate(day, "yyyy-MM-dd");
        if (dayTime == null) {
            Asserts.failInvalidParam("Invalid day param");
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(dayTime);
        int i = 0;
        do {
            Date time = calendar.getTime();
            String hour = TimeUtil.dateToStr(time, "yyyy-MM-dd HH:mm:ss");
            calendar.add(Calendar.HOUR_OF_DAY, 1);
            hours[i] = hour;
            i++;
        } while (i < 25);

        return hours;

    }

    /**
     * Answer the time difference between the given two time
     * in certain format.
     *
     * @param date1 begin time
     * @param date2 end time
     * @return the time diff
     */
    public static String timeDiff(Date date1, Date date2) {
        if (date1 == null || date2 == null || date2.before(date1)) {
            return "";
        }

        long diff = (date2.getTime() - date1.getTime()) / 1000; // in seconds
        long days = TimeUnit.SECONDS.toDays(diff);
        long hours = TimeUnit.SECONDS.toHours(diff) - TimeUnit.DAYS.toHours(days);
        long minutes = TimeUnit.SECONDS.toMinutes(diff) - TimeUnit.DAYS.toMinutes(days)
                - TimeUnit.HOURS.toMinutes(hours);
        long seconds = diff - TimeUnit.DAYS.toSeconds(days) - TimeUnit.HOURS.toSeconds(hours)
                - TimeUnit.MINUTES.toSeconds(minutes);

        StringBuilder builder = new StringBuilder();
        builder.append(days == 0L ? "" : String.valueOf(days) + "天")
                .append(hours == 0L ? "" : String.valueOf(hours) + "小时")
                .append(minutes == 0L ? "" : String.valueOf(minutes) + "分钟")
                .append(seconds == 0L ? "" : String.valueOf(seconds) + "秒");

        return builder.toString();
    }

    /**
     * Convert Date instance to Calendar instance
     *
     * @param date Date instance
     * @return Calendar instance
     */
    public static Calendar dateToCalendar(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar;
    }

    /**
     * Truncate the end time not to beyond now
     *
     * @param end
     */
    public static Date truncEndTime(Date end) {
        Date now = new Date();
        if (end.after(now)) {
            return now;
        }
        return end;
    }

    /**
     * Parse the string to date range
     *
     * @param rangeStr
     * @return
     */
    public static List<Date> parseDateRange(String rangeStr) {
        String[] strDates = rangeStr.split("-");
        if (strDates.length != 2) {
            return new ArrayList<>();
        }

        String beginDateStr = strDates[0];
        String endDateStr = strDates[1];

        List<Date> dates = new ArrayList<>();
        Date beginDate = strToDate(beginDateStr, "yyyy/MM/dd");
        Date endDate = strToDate(endDateStr, "yyyy/MM/dd");

        if (beginDate == null || endDate == null) {
            return new ArrayList<>();
        }

        dates.add(beginDate);
        dates.add(endDate);

        return dates;
    }

    /**
     * Get the middle time of one hour i.e. 2021-02-01 13:45:54 -> 2021-02-01 13:30:00
     * @param time the time to be transformed
     * @return
     */
    public static Date getMidHour(Date time, Integer offset) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(time);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DATE);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        calendar.set(year, month, day, hour, 30, 0);
        calendar.add(Calendar.HOUR_OF_DAY, offset);
        return calendar.getTime();
    }

    /**
     * Get the begin time of one hour i.e. 2021-02-01 13:45:54 -> 2021-02-01 13:00:00
     * @param time the time to be transformed
     * @return
     */
    public static Date getBeginHour(Date time, Integer offset) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(time);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DATE);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        calendar.set(year, month, day, hour, 0, 0);
        calendar.add(Calendar.HOUR_OF_DAY, offset);
        return calendar.getTime();
    }

    /**
     * convert second to hour, minute, second
     * @param secondTime
     * @return
     */
    public static String secondConvertHMS(Integer secondTime) {
        if (secondTime == null || secondTime == 0) {
            return "0秒";
        }

        if (secondTime < 60) {
            return secondTime + "秒";
        }

        if (secondTime % 3600 == 0) {
            return secondTime / 3600 + "小时0分0秒";
        }

        if (secondTime < 3600) {
            int minuteCheck = secondTime % 60;
            if (minuteCheck == 0) {
                int minute = secondTime / 60;
                return minute + "分0秒";
            } else {
                int minute = secondTime / 60;
                int second = secondTime % 60;

                return minute + "分" + second + "秒";
            }
        }

        int hour = secondTime / 3600;
        int minuteCheck = secondTime % 3600 % 60;
        if (minuteCheck == 0) {
            int minute = secondTime % 3600 / 60;
            return hour + "小时" + minute + "分0秒";
        }

        int minute = secondTime % 3600 / 60;
        int second = secondTime % 3600 % 60;
        return hour + "小时" + minute + "分" + second + "秒";
    }

    /**
     * convert second to minute, second
     * @param secondTime
     * @return
     */
    public static String secondConvertMS(Integer secondTime) {
        if (secondTime == null || secondTime == 0) {
            return "0秒";
        }

        if (secondTime < 60) {
            return secondTime + "秒";
        }

        if (secondTime % 60 == 0) {
            int minute = secondTime / 60;
            return minute + "分0秒";
        } else {
            int minute = secondTime / 60;
            int second = secondTime % 60;
            return minute + "分" + second + "秒";
        }
    }

    /**
     * Add hour-min-sec offset on base time
     *
     * @param date
     * @param offset
     * @return
     */
    public static Date offsetHms(Date date, byte[] offset) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.HOUR_OF_DAY, offset[0]);
        calendar.add(Calendar.MINUTE, offset[1]);
        calendar.add(Calendar.SECOND, offset[2]);
        return calendar.getTime();
    }

}
